<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>WebGPU Simple Static Uniforms</title>
    <style>
        @import url(res/webgpu-lesson.css);
        html, body {
        margin: 0;       /* remove the default margin          */
        height: 100%;    /* make the html,body fill the page   */
        }
        canvas {
        display: block;  /* make the canvas act like a block   */
        width: 100%;     /* make the canvas fill its container */
        height: 100%;
        }
    </style>
</head>
<body>
    <canvas></canvas>
</body>
<script type="module">

    // A random number between [min and max)
    // With 1 argument it will be [0 to min)
    // With no arguments it will be [0 to 1)
    const rand = (min, max) => {
        if (min === undefined) {
            min = 0;
            max = 1;
        } else if (max === undefined) {
            max = min;  
        }
        return min + Math.random() * (max - min);
    };

    // 构建一个圆
    function createCircle(
        {
            radius = 1,
            numSubdivisions = 32,
            innerRadius = 0,
            startAngle = 0,
            endAngle = Math.PI * 2,
        } = {}) {
        // 2 triangles per subdivision, 3 verts per tri, 2 values (xy) each.
        const numVertices = numSubdivisions * 3 * 2;
        const vertexData = new Float32Array(numSubdivisions * 2 * 3 * 2);
    
        let offset = 0;
        const addVertex = (x, y) => {
            vertexData[offset++] = x;
            vertexData[offset++] = y;
        };
    
        // 2 vertices per subdivision
        //
        // 0--1 4
        // | / /|
        // |/ / |
        // 2 3--5
        for (let i = 0; i < numSubdivisions; ++i) {
            const angle1 =
                startAngle + ((i + 0) * (endAngle - startAngle)) / numSubdivisions;
            const angle2 =
                startAngle + ((i + 1) * (endAngle - startAngle)) / numSubdivisions;
    
            const c1 = Math.cos(angle1);
            const s1 = Math.sin(angle1);
            const c2 = Math.cos(angle2);
            const s2 = Math.sin(angle2);
    
            // first triangle
            addVertex(c1 * radius, s1 * radius);
            addVertex(c2 * radius, s2 * radius);
            addVertex(c1 * innerRadius, s1 * innerRadius);
    
            // second triangle
            addVertex(c1 * innerRadius, s1 * innerRadius);
            addVertex(c2 * radius, s2 * radius);
            addVertex(c2 * innerRadius, s2 * innerRadius);
        }
    
        return {
            vertexData,
            numVertices,
        };
    }


    async function main() {
        // WebGPU 是异步 API，所以需要使用 async/await 来处理
        const adapter = await navigator.gpu?.requestAdapter();
        const device = await adapter?.requestDevice();
        if (!device) {
            fail('need a browser that supports WebGPU');
            return;
        }

        const canvas = document.querySelector('canvas');    
        const context = canvas.getContext('webgpu');
        // 询问系统首选的画布格式是什么( rgba8unorm 或 bgra8unorm)。
        // 这其实并不重要，重要的是通过查询，可以让用户的系统以最快的速度运行。
        const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
        // 调用 configure 将format传入 webgpu 画布上下文。
        // 将device传入画布，从而将画布与创建的设备关联起来。
        context.configure({
            device,
            format: presentationFormat,
        });

        const wgslCode = `
                struct OurStruct {
                    color: vec4f,
                    offset: vec2f,
                };
                struct OtherStruct{
                    scale: vec2f,
                };

                struct Vertex {
                    position: vec2f,
                };

                struct VSoutput{
                    @builtin(position) position: vec4f,
                    @location(0) color: vec4f,
                };

                @group(0) @binding(0) var<storage, read> ourStructs: array<OurStruct>;
                @group(0) @binding(1) var<storage, read> otherStructs: array<OtherStruct>;
                @group(0) @binding(2) var<storage, read> pos:array<Vertex>;

                @vertex fn vs(
                    @builtin(vertex_index) vertexIndex : u32,
                    @builtin(instance_index) instanceIndex: u32
                ) -> VSoutput {

                    let ourStruct = ourStructs[instanceIndex];
                    let otherStruct = otherStructs[instanceIndex];

                    var output: VSoutput;
                    output.position = vec4f (pos[vertexIndex].position * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
                    output.color = ourStruct.color;
                    return output;
                }

                // 通过inner-stage变量传递
                @fragment fn fs(input: VSoutput) -> @location(0) vec4f {
                    return input.color;
                }
        `;

        const module = device.createShaderModule({
            label: 'multiple uniforms shaders',
            code: wgslCode,
        });

        const pipeline = device.createRenderPipeline({
            label: 'multiple uniforms pipeline',
            layout: 'auto',
            vertex: {
                module,
            },
            fragment: {
                module,
                targets: [{ format: presentationFormat }],
            },
        });

        const {vertexData, numVertices} = createCircle({
            radius: 0.5,
            innerRadius: 0.25,
        });
        // 顶点缓冲区
        const vertexBuffer = device.createBuffer({
            label: 'vertex buffer',
            size: vertexData.byteLength,
            usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
        });
        device.queue.writeBuffer(vertexBuffer, 0, vertexData);


        const kNumObjects = 100;
        const objectInfos = [];
        
        // create 2 buffers for the uniform values
        const staticBufferSize =
            4 * 4 + // color is 4 32bit floats (4bytes each)
            2 * 4 + // offset is 2 32bit floats (4bytes each)
            2 * 4;  // padding to align the next uniform to 16 bytes
        const scaleSize = 2*4; // scale is 2 32bit floats (4bytes each)
        const staticStorageBufferSize = kNumObjects * staticBufferSize;
        const scaleStorageBufferSize = kNumObjects * scaleSize;

        const staticStorageBuffer = device.createBuffer({
                label: `static storage for objects`,
                size: staticStorageBufferSize,
                usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
            });

        const scaleStorageBuffer = device.createBuffer({
            label: `scale changing storage for objects`,
            size: scaleStorageBufferSize,
            usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
        });

        // offsets to the various uniform values in float32 indices
        const kColorOffset = 0;
        const kOffsetOffset = 4;
        const kScaleOffset = 0;

        const staticStorageValues = new Float32Array(staticStorageBufferSize/4);

        for (let i = 0; i < kNumObjects; ++i) {
            const staticOffset = i * (staticBufferSize/4);
            staticStorageValues.set([rand(), rand(), rand(), 1], staticOffset + kColorOffset);        // set the color
            // 向画布左侧移动 1/4，向下移动 1/8（剪辑空间从 -1 到 1 的宽度为 2 个单位，因此 0.25 是 2 的 1/8）。
            staticStorageValues.set([rand(-0.9, 0.9), rand(-0.9, 0.9)], staticOffset + kOffsetOffset);      // set the offset

            objectInfos.push({
                scale: rand(0.2, 0.5),
            });
 
        }
        // copy values to the GPU
        device.queue.writeBuffer(staticStorageBuffer, 0, staticStorageValues);


        // create a typedarray to hold the values for the storage in JavaScript
        const scaleValues = new Float32Array(scaleStorageBufferSize / 4);

        // 创建一个 bindGroup，将 storageBuffer 绑定到着色器中的 @group(0) @binding(0)
        // LXQ: 虽然都是@group(0) @bingding(0),但会在pass.setBindGroup()中
        const bindGroup = device.createBindGroup({
            label: `bind group for objs`,
            // 0 对应着着色器中的 @group(0)
            layout: pipeline.getBindGroupLayout(0),
            entries: [
                { binding: 0, resource: { buffer: staticStorageBuffer }},
                { binding: 1, resource: { buffer: scaleStorageBuffer }},
                { binding: 2, resource: { buffer: vertexBuffer }},
            ],
        });

        const renderPassDescriptor = {
            label: 'our basic canvas renderPass',
            colorAttachments: [
                {
                    clearValue: [0.3, 0.3, 0.3, 1],
                    // load意思是将纹理的现有内容加载到 GPU 中
                    loadOp: 'clear', // 指定在绘制前将纹理清除为clearValue
                    // storeOp: 'store'表示存储绘制结果。也可儿童discard来丢弃绘制的结果
                    storeOp: 'store', // 
                },
            ],
        };
        
        function render() {
            renderPassDescriptor.colorAttachments[0].view =
                context.getCurrentTexture().createView();

            // LXQ: 把所有要绘制的内容放到一个命令缓冲区中，以提高效率
            // encoder和pass对象只是将命令编码到命令缓冲区中
            const encoder = device.createCommandEncoder({ label: 'our encoder' });
            const pass = encoder.beginRenderPass(renderPassDescriptor);
            pass.setPipeline(pipeline);

            // Set the uniform values in our JavaScript side Float32Array
            const aspect = canvas.width / canvas.height;

            objectInfos.forEach(({scale}, idx) => {
               const offset = idx * (scaleSize/4); 
               scaleValues.set([scale / aspect, scale], offset + kScaleOffset); // set the scale
            });
            // upload all scales at once
            device.queue.writeBuffer(scaleStorageBuffer, 0, scaleValues);

            pass.setBindGroup(0, bindGroup);
            // 对于每个实例，WebGPU 将调用顶点着色器 numVertices 次，
            // vertex_index 设置为 0、1、2，instance_index 设置为 0、kNumObjects - 1。
            pass.draw(numVertices, kNumObjects); 

            pass.end();

            const commandBuffer = encoder.finish();
            device.queue.submit([commandBuffer]);
        }
    
        // ResizeObserver 接口监视 Element 内容盒或边框盒或者 SVGElement 边界尺寸的变化。
        // ResizeObserver 避免了通过回调函数调整大小时，通常创建的无限回调循环和循环依赖项。
        const observer = new ResizeObserver(entries => {
            for (const entry of entries) {
                const canvas = entry.target;
                const width = entry.contentBoxSize[0].inlineSize;
                const height = entry.contentBoxSize[0].blockSize;
                canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
                canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));

                render();
            }
        });

        observer.observe(canvas);
    }

    function fail(msg) {
        // eslint-disable-next-line no-alert
        alert(msg);
    }

    main();
</script>


</html>
